using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using System.IO;
using Skill.Ode;
using Skill.Ode.Geometries;

namespace Samples
{

    #region class DemoFramework
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public abstract class DemoFramework : Microsoft.Xna.Framework.Game
    {
        #region enum RenderStage
        /// <summary>
        /// Defines Stage of DemoFramework
        /// </summary>
        enum RenderStage
        {
            Menu,
            Game
        }
        #endregion

        #region Variables

        /// <summary>
        /// Main menu
        /// </summary>
        public Menu Menu { get; private set; }

        // variables for draw
        private RenderStage stage = RenderStage.Menu;
        private BasicEffect effect = null;
        private VertexDeclaration declarationPNC = null; // position normal colored 
        private VertexDeclaration declarationPC = null; // position colored
        private GraphicsDeviceManager graphics;
        private SpriteBatch spriteBatch;
        private SpriteFont spriteFont;
        public Camera Camera { get; private set; }
        private CameraHandler cameraHandler;
        private bool showHelp = false;

        //containers
        private List<DisplayGeometry> displayGeoms = null;
        private List<DisplayString> displayStrings = null;
        private List<DisplayLine> displayLines = null;

        // Ode        
        private string errorFile = "Errors.txt";

        //Input
        public KeyboardState Keyboard = new KeyboardState();
        public MouseState Mouse = new MouseState();
        private KeyboardMapper Mapper = null;

        //Demo
        private OdeDemo demo;
        /// <summary>
        /// Gets or set current demo
        /// </summary>
        public OdeDemo Demo
        {
            get { return demo; }
            set
            {
                demo = value;
                if (demo != null)
                {
                    stage = RenderStage.Game;
                    ResetCamera();
                    demo.Initialize();
                }
            }
        }
        #endregion

        #region Properties

        /// <summary>
        /// The color used for spheres (default : White)
        /// </summary>
        public Color LightColor { get; private set; }

        /// <summary>
        /// Light direction
        /// </summary>
        public Vector3 LightDirection { get; private set; }

        public float CameraRotationSpeed
        {
            get { return cameraHandler.RotationSpeed; }
            set { cameraHandler.RotationSpeed = value; }
        }

        public float CameraMoveSpeed
        {
            get { return cameraHandler.MoveSpeed; }
            set { cameraHandler.MoveSpeed = value; }
        }

        public bool AutoHandleCamera { get; set; }

        #endregion

        #region Constructor
        /// <summary>
        /// Create a DemoFramework
        /// </summary>
        public DemoFramework()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            displayGeoms = new List<DisplayGeometry>(20);
            displayStrings = new List<DisplayString>(10);
            displayLines = new List<DisplayLine>();
            ResetLight();
            AutoHandleCamera = true;
            Mapper = new KeyboardMapper();
        }
        #endregion

        #region Abstract methods
        protected abstract void CreateMenuItems();
        #endregion

        #region Reset
        /// <summary>
        /// Reset light to initial direction and color
        /// </summary>
        public void ResetLight()
        {
            LightColor = new Color(200, 200, 200);
            Vector3 dir = Vector3.Zero - new Vector3(40, 100, 10);
            dir.Normalize();
            LightDirection = dir;
        }
        /// <summary>
        /// Reset camera to initial direction
        /// </summary>
        public void ResetCamera()
        {
            Camera.Position = new Vector3(0, 0, 5);
            Camera.Yaw = 0;// MathHelper.Pi;
            Camera.Pitch = 0;
        }
        #endregion

        #region Initialize
        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
            Camera = new Camera(new Vector3(0, 5, -20), MathHelper.Pi, -MathHelper.Pi / 20, 0, Vector3.Up, 300);
            Camera.Viewport = GraphicsDevice.Viewport;
            cameraHandler = new CameraHandler(this, Camera, 0.0001f, 0.005f);
            GraphicsDevice.DeviceReset += new EventHandler(GraphicsDevice_DeviceReset);
            GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
            Menu = new Menu(this);
            Menu.Initialize();
            CreateMenuItems();
            Menu.GoToLastDemo();
            InitializePhysic();
            this.IsMouseVisible = true;
            Mapper.MapKeyPress(Keys.Escape, delegate
            {
                if (stage == RenderStage.Menu)
                    Exit();
                else if (stage == RenderStage.Game)
                {
                    if (Demo != null)
                    {
                        Demo.Quit();
                        Demo = null;
                        if (displayGeoms != null)
                        {
                            foreach (DisplayGeometry g in displayGeoms)
                            {
                                g.Dispose();
                            }
                            displayGeoms.Clear();
                        }
                        stage = RenderStage.Menu;
                    }
                }
            });
            Mapper.MapKeyPress(Keys.F1, delegate
            {
                showHelp = !showHelp;
            });
        }
        void GraphicsDevice_DeviceReset(object sender, EventArgs e)
        {
            if (Camera != null)
                Camera.Viewport = GraphicsDevice.Viewport;
        }
        private void InitializePhysic()
        {
            if (File.Exists(errorFile))
                File.Delete(errorFile);
            DError.DebugHandler += new DMessageFunction(DError_Handler);
            DError.ErrorHandler += new DMessageFunction(DError_Handler);
            DError.MessageHandler += new DMessageFunction(DError_Handler);
        }

        void DError_Handler(int errNum, string msg, object data)
        {
            FileStream file = new FileStream(errorFile, FileMode.OpenOrCreate, FileAccess.Write);
            file.Seek(0, SeekOrigin.End);
            StreamWriter writer = new StreamWriter(file);
            writer.WriteLine(DateTime.Now.ToString());
            writer.WriteLine(msg);
            writer.Close();
            file.Close();
            writer = null;
            file = null;
        }
        #endregion

        #region Quit
        protected override void OnExiting(object sender, EventArgs args)
        {
            if (displayGeoms != null)
            {
                foreach (DisplayGeometry g in displayGeoms)
                {
                    g.Dispose();
                }
                displayGeoms.Clear();
            }
            if (demo != null)
                demo.Quit();
            demo = null;
            base.OnExiting(sender, args);
        }
        #endregion

        #region Load and unload contetns
        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            spriteFont = Content.Load<SpriteFont>("Arial");
            effect = new BasicEffect(GraphicsDevice, null);
            effect.LightingEnabled = true;
            effect.AmbientLightColor = new Color(50, 50, 50).ToVector3();
            effect.DirectionalLight0.Enabled = true;
            effect.DirectionalLight0.Direction = LightDirection;
            effect.DirectionalLight0.DiffuseColor = LightColor.ToVector3();
            effect.TextureEnabled = false;
            effect.VertexColorEnabled = true;
            effect.World = Matrix.Identity;
            declarationPNC = new VertexDeclaration(GraphicsDevice, VertexPositionNormalColor.VertexElements);
            declarationPC = new VertexDeclaration(GraphicsDevice, VertexPositionColor.VertexElements);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            if (displayGeoms != null)
            {
                foreach (DisplayGeometry g in displayGeoms)
                {
                    g.Dispose();
                }
                displayGeoms.Clear();
            }
            displayGeoms = null;
            if (effect != null)
                effect.Dispose();
            effect = null;
            if (declarationPNC != null)
                declarationPNC.Dispose();
            declarationPNC = null;

            if (declarationPC != null)
                declarationPC.Dispose();
            declarationPC = null;

            if (Menu != null)
            {
                Menu.SaveLastDemo();
                Menu.Dispose();
            }

            if (spriteBatch != null)
                spriteBatch.Dispose();
            spriteBatch = null;
        }
        #endregion

        #region Update
        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            displayStrings.Clear();
            displayLines.Clear();

            Keyboard = Microsoft.Xna.Framework.Input.Keyboard.GetState();
            Mouse = Microsoft.Xna.Framework.Input.Mouse.GetState();

            displayStrings.Add(new DisplayString() { Color = Color.White, Position = new Vector2(GraphicsDevice.Viewport.Width - 200, 10), Text = string.Format("Memory : {0:###,###}", GC.GetTotalMemory(false)) });
            Mapper.Update(ref Keyboard);
            if (stage == RenderStage.Menu)
                Menu.Update(gameTime);
            else if (stage == RenderStage.Game)
            {
                if (AutoHandleCamera)
                    cameraHandler.Update(gameTime);
                Camera.Update();
                if (Demo != null)
                    Demo.StepDemo(gameTime);
                base.Update(gameTime);
            }
        }
        #endregion

        #region Draw
        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            if (stage == RenderStage.Menu)
            {
                GraphicsDevice.RenderState.FillMode = FillMode.Solid;
                GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
                Menu.Draw(gameTime);
            }
            else if (stage == RenderStage.Game)
            {
                GraphicsDevice.RenderState.FillMode = FillMode.Solid;
                effect.View = Camera.View;
                effect.Projection = Camera.Projection;
                effect.LightingEnabled = true;
                effect.DirectionalLight0.Direction = LightDirection;
                effect.DirectionalLight0.DiffuseColor = LightColor.ToVector3();
                effect.Begin();
                DrawShapes();
                DrawLines();
                DrawHelp();
                effect.End();
            }
            DrawStrings();
            // TODO: Add your drawing code here

            base.Draw(gameTime);
        }

        private void DrawHelp()
        {
            if (showHelp)
            {
                if (demo != null)
                {
                    string[] helps = demo.GetHelpString();
                    if (helps != null && helps.Length > 0)
                    {
                        Vector2 p = new Vector2(10, 10);
                        foreach (string s in helps)
                        {
                            DrawString(new DisplayString() { Color = Color.White, Position = p, Text = s });
                            p.Y += spriteFont.LineSpacing + 1;
                        }
                    }
                }
            }
        }

        private void DrawLines()
        {
            GraphicsDevice.VertexDeclaration = declarationPC;
            if (displayLines.Count > 0)
            {
                VertexPositionColor[] vertices = new VertexPositionColor[displayLines.Count * 2];
                int index = 0;
                for (int i = 0; i < displayLines.Count; i++)
                {
                    vertices[index].Position = displayLines[i].From;
                    vertices[index++].Color = displayLines[i].Color;
                    vertices[index].Position = displayLines[i].To;
                    vertices[index++].Color = displayLines[i].Color;
                }

                effect.Alpha = 1;
                effect.DiffuseColor = Color.White.ToVector3();
                effect.LightingEnabled = false;
                effect.World = Matrix.Identity;
                effect.CurrentTechnique.Passes[0].Begin();
                effect.GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineList, vertices, 0, displayLines.Count);
                effect.CurrentTechnique.Passes[0].End();
            }
        }

        private void DrawStrings()
        {
            spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
            foreach (DisplayString s in displayStrings)
            {
                spriteBatch.DrawString(spriteFont, s.Text, s.Position, s.Color);
            }
            spriteBatch.End();
        }

        private void DrawShapes()
        {
            GraphicsDevice.VertexDeclaration = declarationPNC;
            foreach (DisplayGeometry g in displayGeoms)
            {
                g.Draw(effect);
            }
        }

        public void Add(DisplayGeometry geom)
        {
            if (!displayGeoms.Contains(geom))
                displayGeoms.Add(geom);
        }
        public void Remove(DisplayGeometry geom)
        {
            displayGeoms.Remove(geom);
        }

        public void DrawString(DisplayString str)
        {
            displayStrings.Add(str);
        }
        public void DrawLine(DisplayLine line)
        {
            displayLines.Add(line);
        }
        public void DrawArrow(Vector3 start, Vector3 direction, Color color)
        {
            Vector3 to = start + direction * 3;
            DrawLine(new DisplayLine() { Color = color, From = start, To = to });

            Vector3 dir1 = Vector3.TransformNormal(-direction, Matrix.CreateFromAxisAngle(Camera.Frustum.Near.Normal, MathHelper.Pi / 6));
            Vector3 dir2 = Vector3.TransformNormal(-direction, Matrix.CreateFromAxisAngle(Camera.Frustum.Near.Normal, -MathHelper.Pi / 6));
            dir1.Normalize();
            dir2.Normalize();

            DrawLine(new DisplayLine() { Color = color, From = to, To = to + dir1 * 0.5f });
            DrawLine(new DisplayLine() { Color = color, From = to, To = to + dir2 * 0.5f });
        }
        #endregion
    }
    #endregion
}
